/*
XPTracker - Alternative Agile Tracking Tool
Copyright (C) 2006-2009  Stephen Starkey

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
*/
package org.xptracker.search;

import org.apache.commons.lang.StringUtils;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.Store;
import org.xptracker.domain.SearchField;
import org.xptracker.util.ConcurrentMethodCache;

import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ExecutionException;

/**
 * {@link IndexingHelper} which determines fields used for indexing/searching
 * based on the {@link org.xptracker.domain.SearchField} annotation present on public methods of objects.
 *
 * @author caliendo
 */
public class AnnotationDrivenIndexingHelper extends AbstractIndexingHelper {
  private static final String DOCTYPE = "doc.type";

  /**
   * Create a cache which will keep track of the annotated methods for a class.
   */
  private static final ConcurrentMethodCache<Class, Map<String, Method>> ANNOTATED_METHODS =
      new ConcurrentMethodCache<Class, Map<String, Method>>(new ConcurrentMethodCache.Method<Class, Map<String, Method>>() {
        public Map<String, Method> invoke(final Class clazz) {
          final Map<String, Method> methods = new HashMap<String, Method>();

          for (final Method method : clazz.getMethods()) {
            if (method.isAnnotationPresent(SearchField.class)) {
              methods.put(method.getAnnotation(SearchField.class).queryField(), method);
            }
          }

          return methods;
        }
      });

  /**
   * Create a cache which will keep track of the queryFields for a class.
   */
  private static final ConcurrentMethodCache<Class, List<String>> QUERY_FIELDS =
      new ConcurrentMethodCache<Class, List<String>>(new ConcurrentMethodCache.Method<Class, List<String>>() {
        public List<String> invoke(final Class clazz) throws InterruptedException, ExecutionException {
          assert (clazz != null);

          final List<String> keywords = new ArrayList<String>();

          // iterate over all the annotated methods and pull out the keyword
          for (final Method method : getAnnotatedMethods(clazz)) {
            keywords.add(method.getAnnotation(SearchField.class).queryField());
          }

          return keywords;
        }
      });

  private static final ConcurrentMethodCache<Class, List<SearchHelp>> HELP_BEANS =
      new ConcurrentMethodCache<Class, List<SearchHelp>>(new ConcurrentMethodCache.Method<Class, List<SearchHelp>>() {
        public List<SearchHelp> invoke(final Class clazz) throws InterruptedException, ExecutionException {
          assert (clazz != null);

          final List<SearchHelp> searchHelps = new ArrayList<SearchHelp>();

          // iterate over all the annotated methods and pull out the keyword
          for (final Method method : getAnnotatedMethods(clazz)) {
            searchHelps.add(new SearchHelp(method.getAnnotation(SearchField.class)));
          }

          return searchHelps;
        }
      });

  public Document buildDocument(final Object obj) throws RuntimeException {
    assert (obj != null);
    final Document document = new Document();

    document.add(new Field(DOCTYPE, obj.getClass().getName(), Field.Store.YES, Field.Index.UN_TOKENIZED));

    final String idField = getIdField(obj.getClass());
    for (final Method method : getAnnotatedMethods(obj.getClass())) {
      final SearchField searchInfo = method.getAnnotation(SearchField.class);
      final Store storeField = searchInfo.stored() ? Field.Store.YES : Field.Store.NO;
      final Field.Index indexField = searchInfo.queryField().equals(idField) ? Field.Index.UN_TOKENIZED :
          Field.Index.TOKENIZED;
      final String searchValue;

      try {
        searchValue = stringValue(method.invoke(obj));
      } catch (final Exception e) {
        throw new RuntimeException("Unable to determine value for method [" + method + "]", e);
      }

      if (StringUtils.isNotEmpty(searchValue)) {
        document.add(new Field(searchInfo.queryField(), searchValue, storeField, indexField));
      }
    }

    return document;
  }

  private static <T> T softGet(ConcurrentMethodCache<Class, T> cache, final Class clazz) {
    try {
      return cache.get(clazz);
    } catch (final Exception e) {
      cache.remove(clazz); // failed to compute, so clear cache for the class
      throw new RuntimeException("Error getting cached items for class " + clazz, e);
    }
  }

  /**
   * Get the methods for the passed class which are annotated with {@link org.xptracker.domain.SearchField}
   */
  private static Collection<Method> getAnnotatedMethods(final Class clazz) throws RuntimeException {
    return softGet(ANNOTATED_METHODS, clazz).values();
  }

  public List<String> loadQueryFields(final Class clazz) throws RuntimeException {
    return softGet(QUERY_FIELDS, clazz);
  }

  public List<SearchHelp> loadHelpBeans(Class clazz) {
    return softGet(HELP_BEANS, clazz);
  }

  public String getId(Object obj) {
    try {
      if (obj instanceof Document) {
        final Document doc = (Document) obj;
        Class clazz = Class.forName(doc.get(DOCTYPE));
        String idField = getIdField(clazz);
        return doc.get(idField);
      } else {
        final Class clazz = obj.getClass();

        final Map<String, Method> methodCache = softGet(ANNOTATED_METHODS, clazz);
        final Method idMethod = methodCache.get(getIdField(clazz));
        final Object id;
        id = idMethod.invoke(obj);
        return String.valueOf(id);
      }
    } catch (Exception e) {
      throw new RuntimeException("Unable to retrieve ID of item: " + obj, e);
    }
  }

  public String getIdField(Class clazz) {
    return ((Class<? extends Object>) clazz).getAnnotation(SearchedItem.class).idField();
  }
}
